package server;

import helpers.Card;
import helpers.CardDeck;
import helpers.Color;
import helpers.GameOverException;
import helpers.MoveStats;
import helpers.PlayerHintData;
import helpers.Value;
import helpers.action.Action;
import helpers.action.CallAction;
import helpers.action.CheckAction;
import helpers.action.FoldAction;
import helpers.action.RaiseAction;
import helpers.enums.CoTrafiles;
import helpers.enums.RuchPrzeciwnika;
import helpers.enums.Suggestion;
import helpers.enums.UkladNaFlopie;
import helpers.enums.ZachowaniePrzedFlopem;

import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import remote.PokerPlayer;
import cheat.BayesNetworkHelper;

public class PokerArbiter implements Runnable {

	private static final int STARTING_CASH = 1000;
	private static final int SMALL_BLIND = 5;
	private static final int BIG_BLIND = 10;

	private final PokerPlayer first;
	private final PokerPlayer second;

	private int firstCash;
	private int secondCash;

	private int firstPlayerCashOnTable = 0;
	private int secondPlayerCashOnTable = 0;

	private List<Card> cardsOnTable = new ArrayList<Card>();
	private Card p1c1;
	private Card p1c2;
	private Card p2c1;
	private Card p2c2;
	private CardDeck cardDeck;

	private MoveStats p1MoveStats = new MoveStats();
	private MoveStats p2MoveStats = new MoveStats();

	public PokerArbiter(PokerPlayer first, PokerPlayer second)
			throws RemoteException {
		this.first = first;
		this.second = second;
	}

	@Override
	public void run() {
		cardDeck = new CardDeck();

		p1c1 = cardDeck.getRandomCard();
		p1c2 = cardDeck.getRandomCard();

		p2c1 = cardDeck.getRandomCard();
		p2c2 = cardDeck.getRandomCard();

		firstCash = STARTING_CASH;
		secondCash = STARTING_CASH;

		try {
			giveCards();
			afterBlindBet();

			cardsOnTable.add(cardDeck.getRandomCard());
			cardsOnTable.add(cardDeck.getRandomCard());
			cardsOnTable.add(cardDeck.getRandomCard());

			reportStatus();
			hintProposition();

			bettingMoney();
			cardsOnTable.add(cardDeck.getRandomCard());
			reportStatus();
			bettingMoney();
			cardsOnTable.add(cardDeck.getRandomCard());
			reportStatus();
			bettingMoney();
			findTheWinner();
		} catch (RemoteException e) {
			try {
				first.inform("Opponet left the game, you won.");
				second.inform("Opponet left the game, you won.");
			} catch (RemoteException e1) {
				// ignore
			}
		} catch (GameOverException e) {
			// game over
		}
	}

	private void giveCards() throws RemoteException {

		first.inform("Your money: " + firstCash + "\n");
		second.inform("Your money: " + secondCash + "\n");

		first.giveHand(p1c1, p1c2);
		second.giveHand(p2c1, p2c2);

		firstPlayerCashOnTable = SMALL_BLIND;
		first.inform("You posted small blind (" + SMALL_BLIND + ")");
		secondPlayerCashOnTable = BIG_BLIND;
		second.inform("You posted big blind (" + BIG_BLIND + ")");
	}

	private void afterBlindBet() throws RemoteException, GameOverException {
		first.informAboutOpponentMove("Opponent posted big blind (" + BIG_BLIND
				+ ")");

		// player with small blind actions
		List<Action> possibleActions = new ArrayList<Action>();
		possibleActions.add(new CallAction(secondPlayerCashOnTable
				- firstPlayerCashOnTable));
		possibleActions.add(new RaiseAction(secondPlayerCashOnTable
				- firstPlayerCashOnTable, firstCash - firstPlayerCashOnTable));
		possibleActions.add(new FoldAction());
		Action fsa = first.chooseAction(possibleActions);
		onPlayerAction(first, fsa);
		if (fsa instanceof CallAction) {
			p1MoveStats.setSprawdzilesBB(true);
		}

		// player with big blind actions
		possibleActions.clear();
		if (secondPlayerCashOnTable == firstPlayerCashOnTable) {
			possibleActions.add(new CheckAction());
		}
		if (secondPlayerCashOnTable < firstPlayerCashOnTable) {
			possibleActions.add(new CallAction(firstPlayerCashOnTable
					- secondPlayerCashOnTable));
		}
		possibleActions
				.add(new RaiseAction(firstPlayerCashOnTable
						- secondPlayerCashOnTable, secondCash
						- secondPlayerCashOnTable));
		possibleActions.add(new FoldAction());
		Action ssa = second.chooseAction(possibleActions);
		onPlayerAction(second, ssa);
		if (ssa instanceof CallAction) {
			p2MoveStats.setSprawdzilesRaise(true);
			p1MoveStats.setOstatniPodbiles(true);
		}

		if (firstPlayerCashOnTable < secondPlayerCashOnTable) {
			infiniteRaise();
		}
	}

	private void infiniteRaise() throws RemoteException, GameOverException {
		PokerPlayer players[] = { first, second };
		int i = 0;
		while (reRaisePhase(players[i])) {
			i = 1 - i;
		}
	}

	private boolean reRaisePhase(PokerPlayer player) throws RemoteException,
			GameOverException {
		List<Action> possibleActions = new ArrayList<Action>();
		int myMoney = first == player ? firstCash - firstPlayerCashOnTable
				: secondCash - secondPlayerCashOnTable;
		int enemyMoney = second == player ? firstCash - firstPlayerCashOnTable
				: secondCash - secondPlayerCashOnTable;

		possibleActions.add(new RaiseAction(myMoney - enemyMoney, myMoney));
		possibleActions.add(new CallAction(myMoney - enemyMoney));
		possibleActions.add(new FoldAction());

		Action selectedAction = player.chooseAction(possibleActions);
		onPlayerAction(player, selectedAction);

		if (!(selectedAction instanceof RaiseAction)) {
			if (player == first) {
				p1MoveStats.setSprawdzilesReRaise(true);
				p2MoveStats.setOstatniPodbiles(true);
			} else {
				p2MoveStats.setSprawdzilesReRaise(true);
				p1MoveStats.setOstatniPodbiles(true);
			}
			return false;
		}
		return true;

	}

	private void hintProposition() throws RemoteException {
		PlayerHintData playerHintData = first.askAboutHint();
		if (playerHintData != null) {
			Suggestion suggestion = computeSuggestion(first, playerHintData);
			first.presentSuggestiion(suggestion);
		}
		playerHintData = second.askAboutHint();
		if (playerHintData != null) {
			Suggestion suggestion = computeSuggestion(second, playerHintData);
			second.presentSuggestiion(suggestion);
		}
	}

	private Suggestion computeSuggestion(PokerPlayer player,
			PlayerHintData playerHintData) {
		BayesNetworkHelper bayesNetworkHelper = new BayesNetworkHelper();
		bayesNetworkHelper.setCzyPrzeciwnikJestDobry(playerHintData
				.isCzyPrzeciwnikJestDobry());
		bayesNetworkHelper.setCzyStosujeContinuation(playerHintData
				.getCzyStosujeContinuation());
		bayesNetworkHelper
				.setIleRakRozgrywa(playerHintData.getIleRakRozgrywa());
		bayesNetworkHelper.setUkladNaFlopie(computeUkladNaFlopie());
		bayesNetworkHelper.setNaIleSposobowStreet(computeNaIleSposobowStreet());
		bayesNetworkHelper.setIleKolorowNaStole(computeIleKolorowNaStole());
		bayesNetworkHelper.setCoTrafiles(computeCoTrafiles(player));
		bayesNetworkHelper
				.setGlebokoscTwojegoStosu(computeGlebokoscTwojegoStosu(player));
		bayesNetworkHelper
				.setZachowaniePrzedFlopem(computeZachowaniePrzedFlopem(player));
		bayesNetworkHelper
				.setIleWysokichKartNaFlopie(computeIleWysokichKartNaFlopie());
		// bayesNetworkHelper.setRuchPrzeciwnika(computeRuchPrzeciwnika(player));
		return bayesNetworkHelper.getSuggestion();
	}

	private UkladNaFlopie computeUkladNaFlopie() {
		Map<Value, Integer> cardsMap = new HashMap<Value, Integer>();
		for (Card c : cardsOnTable) {
			if (!cardsMap.containsKey(c.getV())) {
				cardsMap.put(c.getV(), 1);
			} else {
				cardsMap.put(c.getV(), cardsMap.get(c.getV()) + 1);
			}
		}
		int max = 0;
		for (Integer v : cardsMap.values()) {
			if (v > max) {
				max = v;
			}
		}
		if (max == 3) {
			return UkladNaFlopie.TROJKA;
		} else if (max == 2) {
			return UkladNaFlopie.PARA;
		} else {
			return UkladNaFlopie.NIC;
		}
	}

	private int computeNaIleSposobowStreet() {
		if (computeUkladNaFlopie() != UkladNaFlopie.NIC) {
			return 0;
		} else {
			int min = 12;
			int max = 0;
			for (Card c: cardsOnTable) {
				min = Math.min(c.getV().ordinal(), min);
				max = Math.max(c.getV().ordinal(), max);
			}
			switch (max-min) {
			case 2: return 3;
			case 3: return 2;
			case 4: return 1;
			default: return 0;
			}
		}
	}

	private int computeIleKolorowNaStole() {
		Set<Color> colors = new HashSet<Color>();
		for (Card c : cardsOnTable) {
			colors.add(c.getC());
		}
		return colors.size();
	}

	private CoTrafiles computeCoTrafiles(PokerPlayer pokerPlayer) {
		List<Card> cards = new ArrayList<Card>();
		cards.addAll(cardsOnTable);
		if (pokerPlayer == first) {
			cards.add(p1c1);
			cards.add(p1c2);
		} else {
			cards.add(p2c2);
			cards.add(p2c1);
		}
		String combination = getCombination(cards);
		if (combination.charAt(0) <= 'W') {
			return CoTrafiles.PARA2;
		} else {
			if (combination.charAt(0) == 'X') {
				if (Integer.parseInt(combination.substring(1, combination.indexOf('_'))) >= Value.QUEEN.ordinal()) {
					int fu = combination.indexOf('_');
					int su = combination.indexOf('_', fu+1);
					if (Integer.parseInt(combination.substring(fu+1, su)) >= Value.QUEEN.ordinal()) {
						return CoTrafiles.TOP_PARA_WYSOKI_KICKER;
					} else {
						return CoTrafiles.TOP_PARA_NISKI_KICKER;
					}
				} else {
					return CoTrafiles.INNA_PARA;
				}
			} else {
				boolean powerful = Integer.parseInt(combination.substring(1, combination.indexOf('_'))) >= Value.QUEEN.ordinal();
				return powerful ? CoTrafiles.WYSOKA_KARTA : CoTrafiles.NIC;
				// TODO compute draws
			}
		}
	}

	private int computeGlebokoscTwojegoStosu(PokerPlayer pokerPlayer) {
		if (pokerPlayer == first) {
			if (firstCash - firstPlayerCashOnTable < 3 * BIG_BLIND) {
				return 0;
			} else {
				return 1;
			}
		} else {
			if (secondCash - secondPlayerCashOnTable < 3 * BIG_BLIND) {
				return 0;
			} else {
				return 1;
			}
		}
	}

	private ZachowaniePrzedFlopem computeZachowaniePrzedFlopem(
			PokerPlayer player) {
		MoveStats moveStats = player == first ? p1MoveStats : p2MoveStats;
		if (moveStats.isOstatniPodbiles()) {
			return ZachowaniePrzedFlopem.OSTATNI_PODBILES;
		} else if (moveStats.isSprawdzilesRaise()) {
			return ZachowaniePrzedFlopem.SPRAWDZILES_RAISE;
		} else if (moveStats.isSprawdzilesReRaise()) {
			return ZachowaniePrzedFlopem.SPRAWDZILES_RE_RAISE;
		} else if (moveStats.isSprawdzilesBB()) {
			return ZachowaniePrzedFlopem.SPRAWDZILES_LUB_CZEKALES_NA_BIG_BLIND;
		} else {
			return ZachowaniePrzedFlopem.INNA_SYTUACJA;
		}

	}

	private int computeIleWysokichKartNaFlopie() {
		int ile = 0;
		for (Card c : cardsOnTable) {
			if (c.getV().ordinal() > Value.N10.ordinal()) {
				ile++;
			}
		}
		return ile;
	}

	private RuchPrzeciwnika computeRuchPrzeciwnika(PokerPlayer player) {
		// TODO check enemy move
		return null;
	}

	private void findTheWinner() throws RemoteException, GameOverException {
        List<Card> p1all = new ArrayList<Card>(cardsOnTable);
        p1all.add(p1c1);
        p1all.add(p1c2);
        String best1 = "Z0";
        for (Set<Card> comb : combinate(5, new HashSet<Card>(p1all))) {
            best1 = betterOf(best1, getCombination(new ArrayList<Card>(comb)));
        }
        
    }

    private String betterOf(String a, String b) {
		if (a.charAt(0) == b.charAt(0)) {
			int ua = a.indexOf('_');
			int ub = b.indexOf('_');
			int ia;
			int ib;
			if (ua == -1) {
				ia = Integer.parseInt(a.substring(1));
				ib = Integer.parseInt(b.substring(1));
			} else {
				ia = Integer.parseInt(a.substring(1, ua));
				ib = Integer.parseInt(b.substring(1, ub));
			}
			if (ia == ib) {
				if (ua == -1) {
					return a;
				} else {
					int ua2 = a.indexOf('_', ua+1);
					int ub2 = b.indexOf('_', ub+1);
					if (ua == -1) {
						ia = Integer.parseInt(a.substring(ua+1));
						ib = Integer.parseInt(b.substring(ub+1));
					} else {
						ia = Integer.parseInt(a.substring(ua+1, ua2));
						ib = Integer.parseInt(b.substring(ub+1, ub2));
					}
					if (ia > ib) {
						return b;
					} else {
						return a;
					}
				}
			} else {
				if (ia < ib) {
					return a;
				} else {
					return b;
				}
			}
		} else {
			if (a.charAt(0) < b.charAt(0)) {
				return a;
			} else {
				return b;
			}
		}
	}

	private static Set<Set<Card>> combinate(int remaining, Set<Card> using) {
        Set<Set<Card>> ret = new HashSet<Set<Card>>();
        for (Card card : using) {
            Set<Card> others = new HashSet<Card>(using);
            others.remove(card);
            if (remaining > 1) {
                for (Set<Card> comb : combinate(remaining - 1, others)) {
                    Set<Card> n = new HashSet<Card>(comb);
                    n.add(card);
                    ret.add(n);
                }
            } else {
                HashSet<Card> n = new HashSet<Card>();
                n.add(card);
                ret.add(n);
            }
        }
        return ret;
    }

    /**
     * A - poker, potem 0 do 12 najwyzsza karta, np. A10 
     * C - kareta, potem 0 do 12 karta karety_kicker, np. C12_10
     * F - full, potem 0 do 12 karta trojki_dwojki, np. F2_3
     * K - kolor, potem 0 do 12 najwyzsza karta, np. K9
     * S - street, potem 0 do 12 najwyzsza karta, np. S7
     * T - trojka, potem 0 do 12 karta_kicker, np. T4_3
     * W - 2 pary, potem 0 do 12 karta wyzszej pary_nizszej_kicker, np. W4_2
     * X - 1 para, potem 0 do 12 karta_kicker, np. X_9_10
     * Z - karta, potem 0 do 12 karta
     */
    private String getCombination(List<Card> cards) {
        boolean inOneColor = true;
        for (Card c : cards) {
            if (c.getC() != cards.get(0).getC()) {
                inOneColor = false;
                break;
            }
        }

        boolean inRow = true;
        int last = cards.get(0).getV().ordinal() - 1;
        for (Card c : cards) {
            if (c.getV().ordinal() != last + 1) {
                inRow = false;
                break;
            }
            ++last;
        }
        if (!inRow) {
            inRow = true;
            last = cards.get(0).getV().ordinal() - 1;
            for (Card c : cards) {
                if (c.getV().ordinal() != last - 1) {
                    inRow = false;
                    break;
                }
                ++last;
            }
        }

        int highestValue = 0;
        for (Card c : cards) {
            highestValue = Math.max(highestValue, c.getV().ordinal());
        }

        if (inRow) {
            if (inOneColor) {
                return "A" + highestValue;
            } else {
                return "S" + highestValue;
            }
        } else {
            if (inOneColor) {
                return "K" + highestValue;
            } else {

                List<Card> bestSames = bestSamesIn(cards);
                if (bestSames.size() == 4) {
                	List<Card> cardsWithout = new ArrayList<Card>(cards);
                	cardsWithout.removeAll(bestSames);
                	return "C"+bestSames.get(0).getV().ordinal()+"_"+cardsWithout.get(0).getV().ordinal();
                } else {
	                if (bestSames.size() == 3) {
	                	List<Card> cardsWithout = new ArrayList<Card>(cards);
	                	cardsWithout.removeAll(bestSames);
						List<Card> isFull = bestSamesIn(cardsWithout);
						if (isFull.size() == 2) {
							return "F"+bestSames.get(0).getV().ordinal()+"_"+isFull.get(0).getV().ordinal();
						} else {
							return "T"+bestSames.get(0).getV().ordinal()+"_"+Math.max(isFull.get(0).getV().ordinal(), isFull.get(1).getV().ordinal());
						}
	                } else {
	                	if (bestSames.size() == 2) {
	                    	List<Card> cardsWithout = new ArrayList<Card>(cards);
	                    	cardsWithout.removeAll(bestSames);
	                    	List<Card> are2 = bestSamesIn(cardsWithout);
	                    	if (are2.size() == 2) {
	                    		ArrayList<Card> lasts = new ArrayList<Card>(cardsWithout);
	                    		lasts.removeAll(are2);
	                    		return "W"+Math.max(bestSames.get(0).getV().ordinal(), are2.get(0).getV().ordinal())+"_"+Math.min(bestSames.get(0).getV().ordinal(), are2.get(0).getV().ordinal())+"_"+lasts.get(0).getV().ordinal();
	                    	} else {
	                    		List<Integer> sorted = new ArrayList<Integer>();
	                    		for (Card c: cardsWithout) {
	                    			sorted.add(c.getV().ordinal());
	                    		}
	                    		Collections.sort(sorted);
	                    		return "X"+bestSames.get(0)+"_"+sorted.get(2)+"_"+sorted.get(1);
	                    	}
	                	} else {
                    		List<Integer> sorted = new ArrayList<Integer>();
                    		for (Card c: cards) {
                    			sorted.add(c.getV().ordinal());
                    		}
                    		Collections.sort(sorted);
	                		return "Z"+sorted.get(4)+"_"+sorted.get(3)+"_"+sorted.get(2);
	                	}
	                }
                }
            }
        }
    }

    private List<Card> bestSamesIn(List<Card> cards) {
        List<Card> bests = new ArrayList<Card>();
        for (Card c: cards) {
            List<Card> sames = samesAsIn(c, cards);
            if (sames.size() > bests.size()) {
            	bests = sames;
            }
        }
        return bests;
    }

	private List<Card> samesAsIn(Card as, List<Card> cards) {
		List<Card> ret = new ArrayList<Card>();
		for (Card c: cards) {
			if (c.getV() == as.getV()) {
				ret.add(c);
			}
		}
		return ret;
	}

	private void onPlayerAction(PokerPlayer player, Action selectedAction)
			throws RemoteException, GameOverException {
		PokerPlayer opponent = first == player ? second : first;
		if (player == first) {
			firstPlayerCashOnTable += selectedAction.getMoneyChange();
		} else {
			secondPlayerCashOnTable += selectedAction.getMoneyChange();
		}

		opponent.informAboutOpponentMove("Opponent action: "
				+ selectedAction.toString());
		if (selectedAction instanceof FoldAction) {
			gameOver(opponent, player);
		}
	}

	private void bettingMoney() throws RemoteException, GameOverException {
		// first player
		List<Action> possibleActions = new ArrayList<Action>();
		possibleActions.add(new CheckAction());
		possibleActions.add(new RaiseAction(secondPlayerCashOnTable
				- firstPlayerCashOnTable, firstCash - firstPlayerCashOnTable));
		possibleActions.add(new FoldAction());
		onPlayerAction(first, first.chooseAction(possibleActions));

		// second player
		possibleActions.clear();
		if (secondPlayerCashOnTable == firstPlayerCashOnTable) {
			possibleActions.add(new CheckAction());
		}
		if (secondPlayerCashOnTable < firstPlayerCashOnTable) {
			possibleActions.add(new CallAction(firstPlayerCashOnTable
					- secondPlayerCashOnTable));
		}
		possibleActions
				.add(new RaiseAction(firstPlayerCashOnTable
						- secondPlayerCashOnTable, secondCash
						- secondPlayerCashOnTable));
		possibleActions.add(new FoldAction());
		onPlayerAction(second, second.chooseAction(possibleActions));

		infiniteRaise();
	}

	private void gameOver(PokerPlayer winner, PokerPlayer loser)
			throws GameOverException, RemoteException {
		winner.inform("***Congratulations, you have won***");
		loser.inform("***You lost***");
		throw new GameOverException();
	}

	private void reportStatus() throws RemoteException {
		first.inform(getGameStatus(true));
		second.inform(getGameStatus(false));
	}

	private String getGameStatus(boolean first) {

		StringBuilder builder = new StringBuilder();
		builder.append("\n").append("----------------------------")
				.append("\n");
		builder.append("Your money: ").append(
				first ? firstCash - firstPlayerCashOnTable : secondCash
						- secondPlayerCashOnTable);
		builder.append(", Opponent money: ").append(
				first ? secondCash - secondPlayerCashOnTable : firstCash
						- firstPlayerCashOnTable);
		builder.append(
				", Money on table: "
						+ (firstPlayerCashOnTable + secondPlayerCashOnTable))
				.append("\n");
		builder.append("Cards on table: ").append("\n\t");
		for (Card card : cardsOnTable) {
			builder.append(card.toString()).append(", ");
		}
		if (cardsOnTable.isEmpty()) {
			builder.append("NONE");
		}
		builder.append("\n");

		builder.append("Your cards:").append("\n\t");
		if (first) {
			builder.append(p1c1.toString() + ", " + p1c2.toString()).append(
					"\n");
		} else {
			builder.append(p2c1.toString() + ", " + p2c2.toString()).append(
					"\n");
		}
		builder.append("\n").append("----------------------------");
		builder.append("\n");
		return builder.toString();
	}

}
